feat(workspace): harden gitworktree adapter for upstream parity#39
feat(workspace): harden gitworktree adapter for upstream parity#39harshitsinghbhandari wants to merge 6 commits into
Conversation
Defense-in-depth so a hung `git fetch` or `worktree add` against an unreachable remote can't block indefinitely when the caller passes `context.Background()`. Caller-supplied deadlines are preserved. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Probe `git remote get-url origin` once per resolve and thread the bool into baseRefCandidates. When origin is absent, the candidate list collapses to local refs only — no wasted `rev-parse origin/...` calls. Preserves the existing qualified-defaultBranch behavior (e.g. "upstream/main" is used as-is rather than re-prefixed with origin/). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Create and Restore now run `git fetch origin --quiet` (swallowed on failure) before resolving the base ref, so new sessions start from current remote tips instead of whatever the working clone last fetched. Gated on `hasOriginRemote` to preserve no-origin and offline flows. Integration test pushes a commit directly to origin via a separate clone and asserts the file appears in the new worktree — proves the fetch actually ran end-to-end. The pusher clone explicitly checks out main to handle CI runners where init.defaultBranch is master. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When `git worktree add` fails mid-creation, best-effort `git worktree remove --force <path>` clears any orphan registration or directory the failed add may have left. Cleanup errors are swallowed — the original add failure is the actionable signal. `--force` is safe here because we just attempted creation; no agent has had a chance to commit. Distinct from Destroy's no-force semantics which protect in-progress agent work — enforced via a separate `worktreeRemoveForceArgs` helper, with the existing Destroy no-force regression test still gating against accidental use. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors upstream's cleanupStaleWorkspacePath: when the workspace path
exists but isn't a registered worktree, rmSync the residue and proceed
with `git worktree add`. The data-loss safety invariant ("never delete
a registered worktree") is preserved by the findWorktree shortcut at
the top of Restore, which returns the adopted worktree before this
branch is reached. Regression test
TestRestoreStillRefusesRegisteredResidue locks the invariant in place.
Closes the Restore self-healing gap from the upstream parity audit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
71c4a65 to
442ffb3
Compare
Greptile SummaryThis PR hardens the git-worktree workspace adapter with five targeted improvements, all contained within the
Confidence Score: 5/5Safe to merge; all five hardening changes are well-scoped and the previous reviewer concerns have been addressed with matching tests. All the active reviewer concerns from the prior round (cancelled-context orphan cleanup, double hasOriginRemote call) are addressed with clean implementations and dedicated tests. The one remaining gap is the plain unqualified branch string used as the last candidate in baseRefCandidates, which the PR description describes as being tightened to refs/heads/ but the code does not reflect. commands.go — the final fallback in baseRefCandidates is still a plain branch name rather than refs/heads/ as the PR description states. Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant Workspace
participant Git
Note over Caller,Git: Create / Restore flow (new hardening)
Caller->>Workspace: Create(ctx, cfg)
Workspace->>Git: check-ref-format (validateBranch)
Git-->>Workspace: ok
Workspace->>Git: remote get-url origin (hasOriginRemote)
alt origin exists
Git-->>Workspace: url
Workspace->>Git: fetch origin --quiet (fetchOrigin, swallow err)
Git-->>Workspace: ok / err (ignored)
else no origin
Git-->>Workspace: err (treated as absent)
end
Workspace->>Git: rev-parse refs/heads/branch (localBranch check)
alt local branch exists
Git-->>Workspace: sha
Workspace->>Git: worktree add path branch
alt add fails
Git-->>Workspace: err
Workspace->>Git: worktree remove --force path (cleanupOrphan, fresh ctx)
end
else new branch
Workspace->>Git: rev-parse candidates (resolveBaseRef)
Git-->>Workspace: baseRef
Workspace->>Git: worktree add -b branch path baseRef
alt add fails
Git-->>Workspace: err
Workspace->>Git: worktree remove --force path (cleanupOrphan, fresh ctx)
end
end
Workspace-->>Caller: WorkspaceInfo / err
Reviews (2): Last reviewed commit: "fix(gitworktree): cleanupOrphan uses fre..." | Re-trigger Greptile |
When the caller's ctx is cancelled mid-`worktree add` (e.g. HTTP request aborted), reusing the same ctx for cleanup means `context.WithTimeout(cancelledCtx, _)` returns an already-cancelled child and the `worktree remove --force` subprocess is killed before it runs — leaving the orphan that cleanupOrphan exists to prevent. Use a fresh background context with its own timeout for the cleanup. Regression test TestCreateCleanupRunsWithFreshContextWhenCallerCancelled pre-cancels the caller ctx, runs Create, and asserts the cleanup subprocess actually executed with an uncancelled context. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Lands upstream-parity hardening for the git-worktree workspace adapter from the audit in
~/.me/notes/ao-workspace-worktree-gap.md. All changes stay withinbackend/internal/adapters/workspace/gitworktree/.ctxhas no deadline (523e9d7).git remote get-url originonce; no-origin flows skip remote candidates entirely and tighten the loose plain-<branch>fallback torefs/heads/<branch>(84736d8+643d28adoc-comment).git fetch origin --quietin Create + Restore (try/swallow, gated onhasOriginRemote). Real-git integration test pushes from a separate clone to prove fetch ran end-to-end (1f6b18a+284aca6review fixes).git worktree remove --force <path>on add failure to prevent orphans. Helper kept deliberately distinct fromDestroy's no-force helper (27a545b+1af4a9dextra coverage).findWorktreeshortcut (030bdcb).Safety invariants preserved
Destroystill uses no--forceand refuses if path is still registered post-prune (review item RA).Createdoes NOT adopt upstream's-Breset — divergent local branches are reused as-is to preserve agent commits.EvalSymlinkscanonicalisation unchanged.validatePathComponentID validation unchanged (dots in IDs still allowed).TestDestroyRefusesStillRegisteredPathAndPreservesDirectoryregression test still asserts--forceis never used fromDestroy.Deferred to followup
Items that require touching files outside
gitworktree/(port additions, new domain types, new event sinks) are tracked in #38:findManagedWorkspace+exists,postCreatehooks, structured activity events, per-callWorktreeDir,preflight, Windows-specific paths, tilde expansion,hasOriginRemoteconsolidation.Test plan
go test ./internal/adapters/workspace/gitworktree/...passes locally (39 tests)TestWorkspaceIntegrationCreateFetchesLatestRemoteproves fetch ran end-to-end (separate pusher clone pushes a commit; new worktree contains the post-push file)--forceis never used from Destroy🤖 Generated with Claude Code